package com.region.loadbalancer.group;

import com.region.common.adapter.logging.LoggerAdapter;
import com.region.common.adapter.logging.LoggerAdapterFactory;
import com.region.loadbalancer.listener.ConnectionListener;
import com.region.loadbalancer.monitor.ServerMonitor;
import com.region.loadbalancer.monitor.event.AnomalyEvent;
import com.region.loadbalancer.monitor.event.AutoDetectEvent;
import com.region.loadbalancer.monitor.event.PromptlyEvent;
import com.region.loadbalancer.policy.AllServerBalancerPolicy;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;

/**
 * Group of Servers Manager
 *
 * @author liujieyu
 * @date 2023/5/25 23:58
 * @desciption
 */
public class GroupServerManager {

    private final LoggerAdapter loggerAdapter = LoggerAdapterFactory.getLogger(getClass());

    private GroupServer groupServer;

    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public GroupServerManager(GroupServer groupServer) {
        this.groupServer = groupServer;
        if (loggerAdapter.isDebugEnabled()) {
            loggerAdapter.debug("The group server is " + groupServer);
        }
        /**
         * Enabling automatic detection of service connectivity.
         * It is recommended not to do so, as it is a timed query and therefore has a performance drain on the program.
         */
        if (this.groupServer.isAutoDetect()) {
            ServerMonitor.getInstance().submitAsyncEvent(new AutoDetectEvent(this.groupServer));
        } else {
            ServerMonitor.getInstance().submitAsyncEvent(new PromptlyEvent(this.groupServer));
        }
    }

    /**
     * Executing the connection listener
     *
     * @param server
     */
    public void executeConnectListener(Server server) {
        List<ConnectionListener> listeners = this.groupServer.getListeners();
        if (listeners == null) {
            return;
        }
        listeners.forEach(connectionListener -> {
            connectionListener.connect(this.groupServer.getGroupName(), server);
        });
    }

    /**
     * Executing the disconnection listener
     *
     * @param server
     */
    public void executeDisconnectListener(Server server) {
        List<ConnectionListener> listeners = this.groupServer.getListeners();
        if (listeners == null) {
            return;
        }
        listeners.forEach(connectionListener -> {
            connectionListener.disconnect(this.groupServer.getGroupName(), server);
        });
    }

    /**
     * Get the service node with the specified status
     *
     * @param state
     * @return
     */
    private List<Server> getServersByStatus(ServerState state) {
        if (state == null) {
            return this.groupServer.getServers();
        }
        return this.groupServer.getServers().stream().filter(server -> state == server.getStatus()).collect(Collectors.toList());
    }

    /**
     * Handling exception service nodes
     *
     * @param anomalyServer The anomaly server
     */
    public void handleAnomalyServer(Server anomalyServer) {
        if (loggerAdapter.isDebugEnabled()) {
            loggerAdapter.debug("The anomaly server is " + anomalyServer);
        }
        // Set the service node state to DOWN
        anomalyServer.setStatus(ServerState.DOWN);
        // Performing a disconnected service listener
        executeDisconnectListener(anomalyServer);
        // Submit an abnormal service event
        ServerMonitor.getInstance().submitAsyncEvent(new AnomalyEvent(this.groupServer, anomalyServer));
    }

    /**
     * Select service node
     *
     * @return
     */
    public Server chooseServer() {
        readWriteLock.readLock().lock();
        try {
            if (this.groupServer.isDetectOnBorrow()) {
                ServerMonitor.getInstance().submitSyncEvent(new PromptlyEvent(this.groupServer));
            }
            // The weighting policy needs to capture all services
            List<Server> chooseServers = this.groupServer.getPolicy() instanceof AllServerBalancerPolicy ?
                    this.getServersByStatus(null) : this.getServersByStatus(ServerState.UP);
            if (chooseServers == null || chooseServers.isEmpty()) {
                return null;
            }
            Server selectServer = this.groupServer.getPolicy().choose(chooseServers);
            if (selectServer == null) {
                return null;
            }
            if (loggerAdapter.isDebugEnabled()) {
                loggerAdapter.debug("The select server is " + selectServer);
            }
            // Add number of times choose server
            selectServer.getCount().incrementAndGet();
            return selectServer;
        } finally {
            readWriteLock.readLock().unlock();
        }
    }

    /**
     * Save the current response time of the service
     *
     * @param server
     * @param time
     */
    public void saveServerResponseTime(Server server, double time) {
        double avgTime = server.getAvgTime();
        if (avgTime == 0f) {
            server.setAvgTime(time);
        } else {
            server.setAvgTime((avgTime + time) / 2);
        }
    }

    /**
     * Full detection of service availability
     *
     * @param groupServer
     */
    public void fullDetectServer(GroupServer groupServer) {
        groupServer.getServers().forEach(server -> {
            boolean connect = groupServer.getDetectionConnection().connect(server);
            if (connect) {
                server.setStatus(ServerState.UP);
                groupServer.getManager().executeConnectListener(server);
            } else {
                server.setStatus(ServerState.DOWN);
                groupServer.getManager().executeDisconnectListener(server);
            }
        });
    }

    /**
     * Update the server node information
     *
     * @param servers
     */
    public void updateServer(List<Server> servers) {
        readWriteLock.writeLock().lock();
        try {
            List<Server> sourceServers = groupServer.getServers();
            Map<Server, Integer> removeServer = new HashMap<>();
            for (int i = 0; i < servers.size(); i++) {
                Server server = servers.get(i);
                // Check whether service nodes are consistent
                boolean exists = false;
                for (int j = 0; j < sourceServers.size(); j++) {
                    if (equals(sourceServers.get(j), server)) {
                        exists = true;
                    } else {
                        Integer index = removeServer.get(sourceServers.get(j));
                        removeServer.put(sourceServers.get(j), index == null ? 1 : ++index);
                    }
                }
                if (!exists) {
                    // add new register server
                    sourceServers.add(server);
                }
            }
            // remove down server
            for (Map.Entry<Server, Integer> entry : removeServer.entrySet()) {
                if (entry.getValue().intValue() == servers.size()) {
                    sourceServers.remove(entry.getKey());
                }
            }
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    /**
     * Check whether two service nodes are consistent
     * @param source
     * @param target
     * @return
     */
    private boolean equals(Server source, Server target) {
        if (Objects.equals(source.getProtocol(), target.getProtocol())
            && Objects.equals(source.getHost(), target.getHost())
            && Objects.equals(source.getPrimePath(), target.getPrimePath())
            && source.getPort() == target.getPort()) {
            return true;
        }
        return false;
    }

}
